<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
  <title>ChatGPT</title>

  <link rel="shortcut icon" href="icon.png" />

  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.47/vue.global.prod.js"></script>

  <link rel="stylesheet" type="text/css" href="https://cdn.bootcdn.net/ajax/libs/element-plus/2.2.30/index.css">
  <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.2.30/index.full.min.js"></script>
  
  <script src="https://cdn.bootcdn.net/ajax/libs/marked/4.2.12/marked.min.js"></script>

  <link href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/styles/base16/default-dark.min.css" rel="stylesheet">
  <script src="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>

  <style type="text/css">
    html,body,#app{
      height: 100%;
      margin:0;
    }
    p{
      margin:0;
    }

    .chat{
      background-color: #272a37;
      height: 100%;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
    }
    .chat .head{
      text-align: center;
      padding: 20px;
      color: #fff;
    }

    .chat .body{
      background-color: #323644;
      overflow: auto;
      height:100%; 
    }
    .chat .body::-webkit-scrollbar {
      width: 5px;
    }
    .chat .body::-webkit-scrollbar-track {
      background: #e0e0e0;
      border-radius: 3px;
    }
    .chat .body::-webkit-scrollbar-thumb {
      background-color: #c2c2c2;
      border-radius: 3px;
    }
    .chat-msg{
      display: flex;
      padding:20px 10px 10px 10px;
    }
    .chat .body .assistant{
      justify-content:flex-start;
    }
    .chat .body .assistant .content{
      background-color: #d9d9d9;
    }
    .chat .body .user{
      justify-content:flex-end;
    }
    .chat .body .user .content{
      background-color: #1d90f5;
      color:#fff;
    }
    .chat-msg .content{
      max-width: 70%;
      margin:0 10px;
      padding:10px;
      border-radius: 8px;
    }
    .el-loading-mask{
      border-radius: 8px;
    }

    .chat .footer{
      padding: 20px;
      display: flex;
      justify-content: space-between;
    }
  </style>
</head>

<body>
<div id="app">
  <div class="chat">
    <div class="head">
      ApiKey<el-input v-model="apikey" type="password" style="width: 50%;margin:0 10px;" placeholder="请输入ChatGPT的API密钥"></el-input>
      <el-button type="primary" @click="saveKey">保存到浏览器</el-button>
      <el-button type="primary" @click="delKey">删除保存的密钥</el-button>
      <el-button type="primary" @click="remain">查看余额</el-button>
    </div>

    <div id="chat-body" class="body">
      <!-- 对话内容 -->
      <div v-for="v in msg" class="chat-msg" :class="v.role">
        <el-avatar v-if="v.role=='assistant'" :size="30" src="icon.png"></el-avatar>
        <div v-if="v.role!='system'" class="content" v-html="v.role=='user'?v.content:marked.parse(v.content)"></div>
      </div>
      <!-- AI输入中 -->
      <div v-show="!canSend" class="chat-msg assistant">
        <el-avatar :size="30" src="icon.png"></el-avatar>
        <div class="content animate__animated animate__fadeInUp" v-html="marked.parse(tmpTxt+'\n')"></div>
      </div>
    </div>

    <div class="footer">
      <el-input v-model="input" autosize type="textarea" placeholder="请输入内容" @keypress="pressEnter"></el-input>
      <el-button type="primary" :disabled="!canSend" style="margin-left:20px;" @click="send">发送</el-button>
    </div>
  </div>
</div>

<script type="text/javascript">
  var app=Vue.createApp({
    data(){
      return{
        baseUrl:'https://api.openai.com',
        saveName:'chatgpt-apikey',
        apikey:'',
        msg:[{role:'system',content:'以markdown格式回答'}],//所有对话
        input:'',//输入
        canSend:true,
        tmpTxt:'',//临时文本
      }
    },
    created(){
      this.marked=marked
      this.apikey=localStorage.getItem(this.saveName)
    },
    methods:{
      //查看余额
      remain(){
        this.$loading()
        fetch(this.baseUrl+'/dashboard/billing/credit_grants',{
          method:'GET',
          headers:{
            'Content-Type':'application/json',
            'Authorization': 'Bearer '+this.apikey
          },
        }).then(response => response.json()).then(o => {
          this.$notify({
            title: '余额',
            message: '总计：'+o.total_granted+'，可用：'+o.total_available+'，已用：'+o.total_used,
            duration: 0,
          })
          this.$loading().close()
        }).catch(e=>{
          this.$message.error(e)
          this.$loading().close()
        })
      },
      //回车
      pressEnter(){
        if(event.keyCode == 13){
          event.preventDefault()//禁止默认换行
          this.send()
        }
      },
      //发送消息
      send(){
        //判断apikey
        if(!this.apikey){
          this.$message.error('请输入ChatGPT的API密钥')
          return
        }
        //判断发言
        if(!this.input){
          this.$message.error('请输入问题')
          return
        }

        //显示你的发言
        this.msg.push({role:'user',content:this.input})

        //清空输入框
        this.input=''

        //禁止输入和发送
        this.canSend=false

        //获取元素用于滚动
        let chatBody=document.getElementById('chat-body')

        fetch(this.baseUrl+'/v1/chat/completions',{
          method:'POST',
          headers:{
            'Content-Type':'application/json',
            'Authorization': 'Bearer '+this.apikey
          },
          body:JSON.stringify({
            model: 'gpt-3.5-turbo',
            messages:this.msg,
            temperature: 0.6,//创意度
            stream: true,
          }),
        }).then(o=>{
          let reader = o.body.getReader()

          let readStream=(reader)=>{
            reader.read().then(({ done, value }) => {
              //结束处理
              if (done) {
                this.msg.push({role:'assistant',content:this.tmpTxt})
                this.canSend=true
                this.tmpTxt=''

                //消息长度控制在30
                let spli=this.msg.length-30
                if(spli>0)this.msg.splice(0,spli)

                //滚动到底部
                chatBody.scrollTop = chatBody.scrollHeight

                //代码高亮
                this.$nextTick(() => {hljs.highlightAll()})

                return
              }

              let decodeds = new TextDecoder().decode(value)
              let decodedArray=decodeds.split('data: ')

              decodedArray.forEach(v=>{
                if(!v||v.trim()=='[DONE]')return
                let json=JSON.parse(v)
                let text=json.choices[0].delta.content??''
                if(text)this.tmpTxt+=text
              })

              //滚动到底部
              chatBody.scrollTop = chatBody.scrollHeight

              readStream(reader)
            })
          }

          readStream(reader)
        }).catch(e=>{
          this.$alert(e.message)
        })
      },
      //保存密钥
      saveKey(){
        localStorage.setItem(this.saveName, this.apikey)
        this.$message.success('已保存')
      },
      //删除密钥
      delKey(){
        localStorage.removeItem(this.saveName)
        this.apikey=''
        this.$message.success('成功删除缓存密钥')
      }
    }
  })
  app.use(ElementPlus)
  app.mount("#app")
</script>
</body>
</html>
